/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.support.scripting.impl.js;

import org.mozilla.javascript.*;

import javax.script.Bindings;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a> <br/>
 * @since 2.0.0 <br/>
 */
final class ExternalScriptable implements Scriptable {

	private Bindings context;

	private Map<Object, Object> indexedProps;

	private Scriptable prototype;

	private Scriptable parent;

	ExternalScriptable(Bindings context) {
		this(context, new HashMap<Object, Object>());
	}

	ExternalScriptable(Bindings context, Map<Object, Object> indexedProps) {
		if (context == null) {
			throw new NullPointerException("context is null");
		}
		this.context = context;
		this.indexedProps = indexedProps;
	}

	Bindings getContext() {
		return context;
	}

	private boolean isEmpty(String name) {
		return name.equals("");
	}

	/**
	 * Return the name of the class.
	 */
	public String getClassName() {
		return "Global";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#get(java.lang.String,
	 * org.mozilla.javascript.Scriptable)
	 */
	public synchronized Object get(String name, Scriptable start) {
		if (isEmpty(name)) {
			if (indexedProps.containsKey(name)) {
				return indexedProps.get(name);
			} else {
				return NOT_FOUND;
			}
		} else {
			synchronized (context) {
				Object value = context.get(name);
				return Context.javaToJS(value, this);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#get(int,
	 * org.mozilla.javascript.Scriptable)
	 */
	public synchronized Object get(int index, Scriptable start) {
		Integer key = new Integer(index);
		if (indexedProps.containsKey(index)) {
			return indexedProps.get(key);
		} else {
			return NOT_FOUND;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#has(java.lang.String,
	 * org.mozilla.javascript.Scriptable)
	 */
	public synchronized boolean has(String name, Scriptable start) {
		if (isEmpty(name)) {
			return indexedProps.containsKey(name);
		} else {
			synchronized (context) {
				return context.containsKey(name);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#has(int,
	 * org.mozilla.javascript.Scriptable)
	 */
	public synchronized boolean has(int index, Scriptable start) {
		Integer key = new Integer(index);
		return indexedProps.containsKey(key);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#put(java.lang.String,
	 * org.mozilla.javascript.Scriptable, java.lang.Object)
	 */
	public void put(String name, Scriptable start, Object value) {
		if (start == this) {
			synchronized (this) {
				if (isEmpty(name)) {
					indexedProps.put(name, value);
				} else {
					synchronized (context) {
						context.put(name, jsToJava(value));
					}
				}
			}
		} else {
			start.put(name, start, value);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#put(int,
	 * org.mozilla.javascript.Scriptable, java.lang.Object)
	 */
	public void put(int index, Scriptable start, Object value) {
		if (start == this) {
			synchronized (this) {
				indexedProps.put(new Integer(index), value);
			}
		} else {
			start.put(index, start, value);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#delete(java.lang.String)
	 */
	public synchronized void delete(String name) {
		if (isEmpty(name)) {
			indexedProps.remove(name);
		} else {
			synchronized (context) {
				context.remove(name);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#delete(int)
	 */
	public void delete(int index) {
		indexedProps.remove(new Integer(index));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#getPrototype()
	 */
	public Scriptable getPrototype() {
		return prototype;
	}

	/**
	 * Set the prototype of the object.
	 * 
	 * @param prototype
	 *            the prototype to set
	 */
	public void setPrototype(Scriptable prototype) {
		this.prototype = prototype;
	}

	/**
	 * Get the parent scope of the object.
	 * 
	 * @return the parent scope
	 */
	public Scriptable getParentScope() {
		return parent;
	}

	/**
	 * Set the parent scope of the object.
	 * 
	 * @param parent
	 *            the parent scope to set
	 */
	public void setParentScope(Scriptable parent) {
		this.parent = parent;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#getIds()
	 */
	public synchronized Object[] getIds() {
		String[] keys = getAllKeys();
		int size = keys.length + indexedProps.size();
		Object[] res = new Object[size];
		System.arraycopy(keys, 0, res, 0, keys.length);
		int i = keys.length;
		for (Object index : indexedProps.keySet()) {
			res[i++] = index;
		}
		return res;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.mozilla.javascript.Scriptable#getDefaultValue(java.lang.Class)
	 */
	public Object getDefaultValue(Class<?> typeHint) {
		for (int i = 0; i < 2; i++) {
			boolean tryToString;
			if (typeHint == ScriptRuntime.StringClass) {
				tryToString = (i == 0);
			} else {
				tryToString = (i == 1);
			}

			String methodName;
			Object[] args;
			if (tryToString) {
				methodName = "toString";
				args = ScriptRuntime.emptyArgs;
			} else {
				methodName = "valueOf";
				args = new Object[1];
				String hint;
				if (typeHint == null) {
					hint = "undefined";
				} else if (typeHint == ScriptRuntime.StringClass) {
					hint = "string";
				} else if (typeHint == ScriptRuntime.ScriptableClass) {
					hint = "object";
				} else if (typeHint == ScriptRuntime.FunctionClass) {
					hint = "function";
				} else if (typeHint == ScriptRuntime.BooleanClass
						|| typeHint == Boolean.TYPE) {
					hint = "boolean";
				} else if (typeHint == ScriptRuntime.NumberClass
						|| typeHint == ScriptRuntime.ByteClass
						|| typeHint == Byte.TYPE
						|| typeHint == ScriptRuntime.ShortClass
						|| typeHint == Short.TYPE
						|| typeHint == ScriptRuntime.IntegerClass
						|| typeHint == Integer.TYPE
						|| typeHint == ScriptRuntime.FloatClass
						|| typeHint == Float.TYPE
						|| typeHint == ScriptRuntime.DoubleClass
						|| typeHint == Double.TYPE) {
					hint = "number";
				} else {
					throw Context
							.reportRuntimeError("Invalid JavaScript value of type "
									+ typeHint.toString());
				}
				args[0] = hint;
			}
			Object v = ScriptableObject.getProperty(this, methodName);
			if (!(v instanceof Function))
				continue;
			Function fun = (Function) v;
			Context cx = Context.enter();
			try {
				v = fun.call(cx, fun.getParentScope(), this, args);
			} finally {
				Context.exit();
			}
			if (v != null) {
				if (!(v instanceof Scriptable)) {
					return v;
				}
				if (typeHint == ScriptRuntime.ScriptableClass
						|| typeHint == ScriptRuntime.FunctionClass) {
					return v;
				}
				if (tryToString && v instanceof Wrapper) {
					Object u = ((Wrapper) v).unwrap();
					if (u instanceof String)
						return u;
				}
			}
		}
		String arg = (typeHint == null) ? "undefined" : typeHint.getName();
		throw Context
				.reportRuntimeError("Cannot find default value for object "
						+ arg);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.mozilla.javascript.Scriptable#hasInstance(org.mozilla.javascript.
	 * Scriptable)
	 */
	public boolean hasInstance(Scriptable instance) {
		Scriptable proto = instance.getPrototype();
		while (proto != null) {
			if (proto.equals(this))
				return true;
			proto = proto.getPrototype();
		}
		return false;
	}

	/**
	 * @return
	 */
	private String[] getAllKeys() {
		ArrayList<String> list = new ArrayList<String>();
		synchronized (context) {
			list.ensureCapacity(context.size());
			for (String key : context.keySet()) {
				list.add(key);
			}
		}
		String[] res = new String[list.size()];
		list.toArray(res);
		return res;
	}

	/**
	 * @param jsObj
	 * @return
	 */
	private Object jsToJava(Object jsObj) {
		if (jsObj instanceof Wrapper) {
			Wrapper njb = (Wrapper) jsObj;

			if (njb instanceof NativeJavaClass) {
				return njb;
			}
			Object obj = njb.unwrap();
			if (obj instanceof Number || obj instanceof String
					|| obj instanceof Boolean || obj instanceof Character) {
				return njb;
			} else {
				return obj;
			}
		} else {
			return jsObj;
		}
	}
}
